
package gamepack;

import java.awt.geom.*;
import java.awt.geom.AffineTransform;
import java.awt.*;
import java.awt.image.*;
import javax.swing.*;


public class CImageUtil {

	// Helper function for CImage rotation.
	// If @cimg.img were to be rotated by @angle. It returns the bounding rectangle for the rotated @cimg.img
	public static Rectangle calcBoundingRectangle(int angle,CImage cimg) {
	
		// Create a outline of @cimg (easier to rotate the outline than the image itself)
		Rectangle r = new Rectangle();
		r.x = 0;
		r.y = 0;
		r.width = cimg.img.getWidth(null);
		r.height = cimg.img.getHeight(null);
						
		// Convert the outline to polygon (easier to rotate the polygon by converting them to Points and applying std Java affineTransform.
		Polygon p = rectToPoly( r );// Instead of collision rect, we do it with the Image itself
		Point2D.Double[] pointArr = polyToPoints( p );
		Point2D.Double[] destPts = new Point2D.Double[pointArr.length];
				
		// Transform the pointsArr of the polygon
		AffineTransform ptx = new AffineTransform();
		ptx.rotate( Math.toRadians(angle) , -cimg.x, -cimg.y );
		
		ptx.transform( pointArr , 0, destPts, 0, pointArr.length );
		
		// Convert back to polygon and set Polygon
		Polygon pnew = pointsToPolygon( destPts );
		Rectangle rnew = polyToRect( pnew );
		return rnew; 
	}
	

public static BufferedImage getAffineForImage(String ops,CImage cimg,int angle) { // rotateleft now means rotate
	//Image img = cimg.img;
	BufferedImage img = toBufferedImage( cimg.img );
	AffineTransform tx = new AffineTransform();
	BufferedImage retImg = null;
		
	if( ops.equals("fliph") ) {                   // Image needs to be flipped along the center of the image.
		tx.translate( img.getWidth()/2+1 , 0);     // Observation FLIPH now works perfectly. See below for Flipv.
		tx.scale(-1,1);
		tx.translate( -img.getWidth()/2 , 0);	  
		AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
		retImg = op.filter(img, null);
	} else if ( ops.equals("flipv") ) {           // Observation - The Affine version sometimes returned image 1 pixel shorter in height	
		tx.translate( 0, (img.getHeight()/2)+1 ); // Adding +1 here solved it, and all associated problems, with Both Brick and Tank Images.
		tx.scale(1,-1);                           // FLIPV Now works perfectly fine.
		tx.translate( 0, -(img.getHeight()/2) );
		AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
		retImg = op.filter(img, null);		
	} else if ( ops.equals("rotate") ) {	
		
		// Calculate if the bounding rectangle if the Image was rotated by @angle
		Rectangle br = calcBoundingRectangle( angle, cimg); 
		
		// If bounding rectangle position is Negative, it is observed that the image gets cropped 
		// on the x and y axis when Affine Transform is applied. 
		// So let us translate the affine transform such that the final transformed image's bounding rectangle lies at Point(0,0)
		// This will ensure that the rotated image is not clipped at x=0 and y=0
		// A correction for this translation is provided in the method getAffineForPoint()
		if( br.x < 0 ) { tx.translate( -br.x,0 ); }	
		if( br.y < 0 ) { tx.translate( 0,-br.y ); }		
		// Apply the Affine transform on the Image and return.
		tx.rotate( Math.toRadians(angle) , -cimg.x, -cimg.y );				
		AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
		retImg = op.filter(img, null);
	} 
	return retImg;
}

public static AffineTransform getAffineForPolygon(String ops,CImage cimg,int angle) {
	//Image img = cimg.img;
	BufferedImage img = toBufferedImage( cimg.img );
	AffineTransform tx = new AffineTransform();
	if( ops.equals("fliph") ) {					// The polygon is just mirroed (no axis since polygon is already at origin)
		tx.scale(-1,1);
	} else if ( ops.equals("flipv") ) {    										
		tx.scale(1,-1);							// Observation - Tank's rect.x is 1pixel lower, but anychanges here will affect general
												// vflip which happens correctly for bricks. Hence no changes. In fact changing img is the soln.
	} else if ( ops.equals("rotate") ) {
		tx.translate( cimg.x , cimg.y );
		tx.rotate( Math.toRadians(angle) , -cimg.x, -cimg.y );
		tx.translate( -cimg.x , -cimg.y );		
	} 
	return tx;
}

public static AffineTransform getAffineForPoint(String ops,CImage cimg,int angle) {
	//Image img = cimg.img;
	BufferedImage img = toBufferedImage( cimg.img );
	AffineTransform tx = new AffineTransform();
	if( ops.equals("fliph") ) {                 // the center is mirroed along the axis which is center of the image.
		tx.translate( -(img.getWidth()/2) , 0);
		tx.scale(-1,1);
		tx.translate(  (img.getWidth()/2) , 0);		
	} else if ( ops.equals("flipv") ) {
		tx.translate( 0, -img.getHeight()/2 );
		tx.scale(1,-1);
		tx.translate( 0, (img.getHeight()/2)  );		
	} else if ( ops.equals("rotate") ) {
	
		Rectangle br = calcBoundingRectangle(angle, cimg);	
		// Correction to be applied for the additional translation done in getAffineForImage() method which prevents clipping of Image.
		if( br.x < 0 ) { tx.translate( br.x,0 ); }	
		if( br.y < 0 ) { tx.translate( 0, br.y ); }		
		tx.rotate( Math.toRadians(angle) , 0 );
	} 
	return tx;
}


	
// Thinking Aloud:  Note - the 3 things. Image | Collision Polygon | Anchor Point are not dependent on each 
//        other while calculating their new transformed Positions.
//
//        Observations - 
//		
//		1. The transformation of Image by 2 seperate implementation(Affine and Non Affine) are not always the same. 
//		   Eg. the vflip operation on Weak brick gave different results. The Affine version of the image was 1 pixel shorter in height.
//		   Hence for vflip the nonaffine transform version is used.
//		2. Ideal logic for rotating all 3 (Image,anchor,collision area) would be - 
//		   a) First rotate collision polygon this has no dependency as it is defined wrt blt point. (Simple huh)
//		   b) Second rotate Image, wrt to its anchor point. Now if this rotated image cuts x/y axis we have a problem, since the image is going to 
//		   get cut at that point. (Since this is a display issue, let us not worry about it now. Our calculations are still accurate) 
//			(Logical huh)
//		   c) Rotate the image around 0,0	(confused huh?) I don't understand it properly!!, but it seems to work. There are a few glitches though.
//         d) In step b) the bounding rect obtained gets cut, which can be corrected by translating step c)'s result by the same.
////
//     Now if I were to rotate at arbitrary angle theta, 
//         a) Polygon rotation should be easiy.
//         b) Image rotataion should be easy too.
//		   c) The center rotating sounds confusing to me, but is working for now.
//
//    TODO - AS OF NOW THERE ARE A FEW BUGS WITH THIS IMPLEMENTATION. The image and it collision rectangles are not perfectly in sync.
//			 when rotated, there is a gap of +-1 pixel between the two. Needs to be fixed.
// 
/**	This method should apply the transform @ops on CImage @cimg and return the transformed CImage. The original @cimg should not 
    be altered. While applying the transform on CImage, care must be taken to rotate not just the image but also its collision rectangle
	If it has a subShape, then its subShape and the center/anchor point of the CImage.

	@ops - The transformation operations, supported as of now are - fliph, flipv, rotation.
	@cimg - The CImage over which the transform has to applied.	cimg is not changed but a copy of it is created/changed and returned.
	@angle - If ops='rotation' than angle (in degrees) is used to specify the amount of rotation. 
			 Valid range would be -360 to +360 The angle of rotation is anticlockwise.
*/		
public static CImage affineTransform(String ops, CImage cimg,int angle) {	
    //System.out.println("CImageUtil.affineTransform("+ops+","+angle+") invoked" );
	angle = 360-angle;  // this makes the angle move in anticlockwise direction, which is standard convention.
	
	// Step #1 Transform/ rotate the image. ========================================================================
	BufferedImage bufferedImage = toBufferedImage( cimg.img );
	
	bufferedImage = getAffineForImage(ops,cimg,angle);
	//bufferedImage = getNonAffineImage(ops,cimg);		// Alternate implementation for rotating the image (for comparison/debugging)

	CImage retCImage = new CImage( bufferedImage ); 
	
	// Step #2 handle the collision Area rectangle/Polygon  =================================================

	if( cimg.polygon != null ) { // Start Polygon Handling ==================================================
	
		// Convert Polygon to points
		Point2D.Double[] pointArr = polyToPoints( cimg.polygon );
		Point2D.Double[] destPts = new Point2D.Double[pointArr.length];

		// Transform the points of the polygon
		AffineTransform ptx = getAffineForPolygon(ops,cimg,angle);		
		ptx.transform( pointArr , 0, destPts, 0, pointArr.length );
		
		// Convert back to polygon and set Polygon
		Polygon p = pointsToPolygon( destPts );
		retCImage.setPolygon( p ); // This sets the bounding rect internally.		
	} 
		else {  				// Start Rectangle Handling ===============================================
		
		// System.out.println("CImage has no polygon, Only need to transform its bounding rectangle ");	
		// Convert Rectangle to Polygon then to points
		Polygon p = rectToPoly( cimg.getBounds() );
		Point2D.Double[] pointArr = polyToPoints( p );
		Point2D.Double[] destPts = new Point2D.Double[pointArr.length];
		
		// Transform the pointsArr of the polygon
		AffineTransform ptx = getAffineForPolygon(ops,cimg,angle);		
		ptx.transform( pointArr , 0, destPts, 0, pointArr.length );
		
		// Convert back to polygon and set Polygon
		Polygon pnew = pointsToPolygon( destPts );
			if( angle == 0 || angle == 90 || angle == 180 || angle == 270 ) { // If Quadrant rotation, then CImage will have no subShape.
				Rectangle rnew = polyToRect( pnew );
				retCImage.setRect( rnew );  	// takes care of bouding rect too.
			} else {  // Since CImage was rotated by arbitrary angle, it will have a subShape.
				retCImage.setPolygon( pnew );			
			}								
		} // End Rectangle Handling 
		
		// Step #3 - Transform the center =====================================================================================
		
		Point2D centerPt = new Point2D.Double( (int) cimg.x, (int) cimg.y );  // The point to be transformed.		
		Point2D transPt = new Point2D.Double();								  // The point where transformed Point will be stored.
		AffineTransform pointtx = getAffineForPoint(ops,cimg,angle);		  // get transform for applying on center/anchor point.
		transPt = pointtx.transform( centerPt , transPt );					  // Apply the transform and store the result in transPt
		retCImage.x = (int) transPt.getX() ;								  // Apply the result on the retCImage.	
		retCImage.y = (int) transPt.getY() ;

		
		Rectangle br = calcBoundingRectangle(angle,cimg);
		System.out.println("Rotation at "+ (360-angle) + " anticlockwise." + 
		" Rotated bounding rect:" + br.x + "," + br.y );
	
	return retCImage;
}

// Utility conversion methods below ==============================================
	public static Polygon rectToPoly(Rectangle r) {
		int npoints = 4;
		int[] xpoints = new int[4];
		int[] ypoints = new int[4];
		
		xpoints[0] = r.x;
		ypoints[0] = r.y;
		
		xpoints[1] = r.x + r.width-1;
		ypoints[1] = r.y;
		
		xpoints[2] = r.x + r.width-1;
		ypoints[2] = r.y + r.height-1;
		
		xpoints[3] = r.x;
		ypoints[3] = r.y + r.height-1;
		return new Polygon(xpoints,ypoints,npoints);
	}
	
	public static Rectangle polyToRect(Polygon p) {
		// Assuming the polygon is a rectangle of 4 points.
		Rectangle r = new Rectangle( p.getBounds() );
		r.width++;
		r.height++;
		return r;
	}	

	public static Point2D.Double[] polyToPoints(Polygon p) {
		Point2D.Double[] pts = new Point2D.Double[p.npoints];
		for(int i=0;i<p.npoints;i++) {
			pts[i] = new Point2D.Double( p.xpoints[i], p.ypoints[i] );
		}
		return pts;
	}
	
	public static Polygon pointsToPolygon(Point2D.Double[] pts) {
		int[] xpoints = new int[ pts.length ];
		int[] ypoints = new int[ pts.length ];
		for( int i=0; i<pts.length;i++) {
			xpoints[i] = (int) pts[i].x;		
			ypoints[i] = (int) pts[i].y;
		}
		return new Polygon( xpoints, ypoints, pts.length );
	}

// Not sure if this is the best implementation. It does the expected Job.
public static BufferedImage toBufferedImage(Image image) {
    if (image instanceof BufferedImage) {
		//System.out.println("toBufferedImage(Image img) Invoked - img was already a buffered image!! ");
        return (BufferedImage)image;
    }

    // This code ensures that all the pixels in the image are loaded
    image = new ImageIcon(image).getImage();

    // Determine if the image has transparent pixels; for this method's
    // implementation, see Determining If an Image Has Transparent Pixels
    boolean hasAlpha = hasAlpha(image);

    // Create a buffered image with a format that's compatible with the screen
    BufferedImage bimage = null;
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    try {
        // Determine the type of transparency of the new buffered image
        int transparency = Transparency.OPAQUE;
        if (hasAlpha) {
            transparency = Transparency.BITMASK;
        }

        // Create the buffered image
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        GraphicsConfiguration gc = gs.getDefaultConfiguration();
        bimage = gc.createCompatibleImage(
            image.getWidth(null), image.getHeight(null), transparency);
    } catch (HeadlessException e) {
        // The system does not have a screen
    }

    if (bimage == null) {
        // Create a buffered image using the default color model
        int type = BufferedImage.TYPE_INT_RGB;
        if (hasAlpha) {
            type = BufferedImage.TYPE_INT_ARGB;
        }
        bimage = new BufferedImage(image.getWidth(null), image.getHeight(null), type);
    }
	//System.out.println( "type = " + 	bimage.getType() );
    // Copy image to buffered image
    Graphics g = bimage.createGraphics();

    // Paint the image onto the buffered image
    g.drawImage(image, 0, 0, null);
    g.dispose();

    return bimage;
}

public static boolean hasAlpha(Image image) {
    // If buffered image, the color model is readily available
    if (image instanceof BufferedImage) {
        BufferedImage bimage = (BufferedImage)image;
        return bimage.getColorModel().hasAlpha();
    }

    // Use a pixel grabber to retrieve the image's color model;
    // grabbing a single pixel is usually sufficient
     PixelGrabber pg = new PixelGrabber(image, 0, 0, 1, 1, false);
    try {
        pg.grabPixels();
    } catch (InterruptedException e) { } // Get the image's color model 
	ColorModel cm = pg.getColorModel(); 
	return cm.hasAlpha(); 	
}

}

// Work in progress. Alternate implementation for Image Manipulation.
class NonAffineTranformImpl {
	// This method returns true if the specified image has transparent pixels


public static BufferedImage rotate(BufferedImage img, int angle) {   
        int w = img.getWidth();   
        int h = img.getHeight();   
		
        BufferedImage dimg = dimg = new BufferedImage(w, h, img.getType());   		
        Graphics2D g = dimg.createGraphics();   
        g.rotate(Math.toRadians(angle), w/2, h/2);   
        g.drawImage(img, null, 0, 0);   
        return dimg;   
    }  
	
public static BufferedImage horizontalflip(BufferedImage img) {   
        int w = img.getWidth();   
        int h = img.getHeight();   
        BufferedImage dimg = new BufferedImage(w, h, img.getType());   
        Graphics2D g = dimg.createGraphics();   
        g.drawImage(img, 0, 0, w, h, w, 0, 0, h, null);   
        g.dispose();   
        return dimg;   
    }  
public static BufferedImage verticalflip(BufferedImage img) {   
        int w = img.getWidth();   
        int h = img.getHeight();   
        BufferedImage dimg = dimg = new BufferedImage(w, h, img.getColorModel().getTransparency());   
        Graphics2D g = dimg.createGraphics();   
        g.drawImage(img, 0, 0, w, h, 0, h, w, 0, null);   
        g.dispose();   
        return dimg;   
    }  
public static BufferedImage[] splitImage(BufferedImage img, int cols, int rows) {   
        int w = img.getWidth()/cols;   
        int h = img.getHeight()/rows;   
        int num = 0;   
        BufferedImage imgs[] = new BufferedImage[w*h];   
        for(int y = 0; y < rows; y++) {   
            for(int x = 0; x < cols; x++) {   
                imgs[num] = new BufferedImage(w, h, img.getType());   
                // Tell the graphics to draw only one block of the image   
                Graphics2D g = imgs[num].createGraphics();   
                g.drawImage(img, 0, 0, w, h, w*x, h*y, w*x+w, h*y+h, null);   
                g.dispose();   
                num++;   
            }   
        }   
        return imgs;   
    }  
	
	public static BufferedImage getNonAffineImage(String ops,CImage cimg) {
		BufferedImage img = CImageUtil.toBufferedImage( cimg.img );
		BufferedImage retImg = null;
		if( ops.equals("fliph") ) {                   // Image needs to be flipped along the center of the image.
			retImg = horizontalflip(img);
		} else if ( ops.equals("flipv") ) {
			retImg = verticalflip(img);
		} else if ( ops.equals("rotateleft") ) {	
			retImg = rotate(img,90);
		} else if ( ops.equals("rotateright") ) {
			retImg = rotate(img,270);
		} else if ( ops.equals("rotatehalf") ) {
			retImg = rotate(img,180);		
		}
		return retImg;
	}
}

